VB365 SharePoint Auto-Protect
Add-UnprotectedSitesToJob.ps1 finds SharePoint Online sites in a Veeam Backup for Microsoft 365 (VB365) organization that are not in any backup job, adds them to a chosen backup job, and reports what it did. It is built to run unattended as a Windows scheduled task, so new SharePoint sites get protected without anyone having to remember to add them.
What it does
Each run:
- Finds candidate sites. Asks VB365 for every site it reports as "not in a job" (
Get-VBOOrganizationSite -NotInJob). - Double-checks every job. Reads the selected and excluded items of every job in the organization. A site is skipped if it is:
- already selected in a job, or
- explicitly excluded in a job (someone excluded it on purpose, so the script leaves it alone).
- Applies your skip patterns. Skips any site whose URL matches
-ExcludeUrlPattern(for example, the app catalog or test sites). - Skips deleted sites (optional). If Graph is configured, checks the list of sites that still exist in Microsoft 365 and skips any that have been deleted. If this check fails, the run stops rather than risk adding deleted sites.
- Adds the remaining sites to the target job (up to
-MaxSitesToAdd, if set). - Reports. Writes a log file and a CSV report, and a summary to the Windows Application event log.
Nothing is ever removed from a job. The script only adds.
Files in this folder
| File | Purpose |
|---|---|
Add-UnprotectedSitesToJob.ps1 | The script |
README.md | This file |
graph.cred | (Optional) Encrypted Graph app credential, created during setup |
Logs\ | Created automatically. Holds a .log and .csv for each run |
Requirements
- Run on the VB365 server (or anywhere the VB365 PowerShell module is installed; use
-VBOServerto connect to a remote server). - Windows PowerShell 5.1 (
powershell.exe). - Account permissions: the account running the script needs admin rights on the VB365 server to use the Veeam cmdlets.
- Target job: must be a job that backs up selected items. Entire Organization jobs can't have sites added to them, and the script will refuse to use one.
- For the deleted-site check (optional): an Entra ID (Azure AD) app registration with the Microsoft Graph application permission
Sites.Read.All(admin consent granted) and a client secret.
Parameters
| Parameter | Required | Description |
|---|---|---|
-OrganizationName | Yes | VB365 organization name, e.g. contoso.onmicrosoft.com |
-JobName | Yes | Backup job to add sites to |
-ExcludeUrlPattern | No | Wildcard URL patterns to skip. Separate several with commas: "*/sites/appcatalog*,*/sites/test-*" |
-IncludePersonalSite | No | Also include OneDrive (personal) sites |
-MaxSitesToAdd | No | Safety cap on how many sites to add per run. 0 (default) = no limit |
-GraphTenantId | No | Tenant ID or domain. Set together with -GraphCredentialPath to enable the deleted-site check |
-GraphCredentialPath | No | Path to the saved Graph credential file |
-LogDirectory | No | Where logs and reports go. Default: Logs next to the script |
-LogRetentionDays | No | Delete logs/reports older than this. Default 90. 0 = keep forever |
-EventLogSource | No | Event log source name. Default VB365-SiteAutoProtect |
-VBOServer | No | Only needed if not running on the VB365 server |
-WhatIf | No | Dry run: shows and logs what would be added, changes nothing |
-SetupGraphCredential | — | One-time setup mode for saving the Graph credential (see below) |
Setup
1. Copy the files
Put the script in a folder on the VB365 server, for example C:\Scripts\VB365\.
2. (Optional) Save the Graph credential
Skip this if you don't want the deleted-site check.
The credential file is encrypted with Windows DPAPI, which means only the account that created it, on the machine it was created on, can read it. So log on (or use runas) as the account the scheduled task will run under, then run:
cd C:\Scripts\VB365
.\Add-UnprotectedSitesToJob.ps1 -SetupGraphCredential -GraphCredentialPath "C:\Scripts\VB365\graph.cred"
When prompted:
- User name: the app registration's Application (client) ID
- Password: the client secret
3. Do a dry run
.\Add-UnprotectedSitesToJob.ps1 -OrganizationName "contoso.onmicrosoft.com" -JobName "SharePoint Sites" `
-ExcludeUrlPattern "*/sites/appcatalog*" `
-GraphTenantId "contoso.onmicrosoft.com" -GraphCredentialPath "C:\Scripts\VB365\graph.cred" `
-WhatIf
Open the newest CSV in Logs\ and check the WouldAdd (WhatIf) rows look right. Pay attention to any warning about Entire Organization jobs: if one exists, sites listed as unprotected may actually already be backed up by it.
Version check: VB365 property names can differ between versions. If results look wrong, run
Get-VBOJob | Select *andGet-VBOBackupItem -Job (Get-VBOJob -Name "SharePoint Sites") | Select *and confirm theJobBackupTypeandSite.URLproperties exist on your build.
4. Register the scheduled task
From an elevated PowerShell prompt (edit the values for your environment):
$scriptArgs = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Scripts\VB365\Add-UnprotectedSitesToJob.ps1" ' +
'-OrganizationName "contoso.onmicrosoft.com" -JobName "SharePoint Sites" ' +
'-ExcludeUrlPattern "*/sites/appcatalog*" ' +
'-GraphTenantId "contoso.onmicrosoft.com" -GraphCredentialPath "C:\Scripts\VB365\graph.cred" ' +
'-LogDirectory "C:\Scripts\VB365\Logs"'
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $scriptArgs
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 6am
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 2) -MultipleInstances IgnoreNew -StartWhenAvailable
Register-ScheduledTask -TaskName "VB365 - Auto-protect SharePoint sites" -Action $action -Trigger $trigger `
-Settings $settings -User "DOMAIN\svc-vb365" -Password (Read-Host "Password") -RunLevel Highest
Important:
- Use
-File, not-Command. With-Command, the script's exit code is not passed back to Task Scheduler. - Schedule it to finish before the backup job runs, so newly added sites are picked up by that night's backup.
- The first time it runs, the script creates the event log source, which needs admin rights. If the task account isn't an admin, create the source once by hand from an elevated prompt:
New-EventLog -LogName Application -Source VB365-SiteAutoProtect
Exit codes
Shown as Last Run Result in Task Scheduler.
| Code | Meaning | What to do |
|---|---|---|
0 | Success. Sites were added, or there was nothing to add | Nothing |
1 | Finished, but one or more sites failed to add | Check the Failed rows in the CSV for the error |
2 | Fatal error. The run stopped | Check the log for the FATAL line |
3 | Another run was still in progress, so this one did nothing | Usually harmless. If it repeats, the previous run may be stuck |
Event log
A summary of every run is written to Windows Logs > Application, source VB365-SiteAutoProtect, including the list of sites added. The event ID tells you the result, which makes monitoring alerts easy:
| Event ID | Level | Meaning |
|---|---|---|
| 1000 | Information | Success |
| 1001 | Warning | Some sites failed |
| 1002 | Error | Fatal error |
| 1003 | Warning | Skipped, already running |
Logs and reports
Each run creates two files in the log folder, named with the run's date and time:
AutoProtect_<timestamp>.log: step-by-step log of the run.AutoProtect_<timestamp>.csv: one row per site with its outcome:
| Action | Meaning |
|---|---|
Added | Added to the job |
WouldAdd (WhatIf) | Would have been added (dry run) |
Skipped | Not added; the Detail column says why |
Failed | Tried to add but got an error; the Detail column has the error |
Skip reasons you may see: Already selected in a job, Explicitly excluded in a job, Matched -ExcludeUrlPattern, Not found in Microsoft Graph (deleted?), Over -MaxSitesToAdd limit.
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
| Exit 2: Organization ... not found / Backup job ... not found | Check the names exactly match what VB365 shows |
| Exit 2: Entire Organization job | The target job backs up the whole org. Pick a job that backs up selected items |
| Exit 2: Could not decrypt ... graph.cred | The credential file was created by a different account or on a different machine. Re-run setup as the task account |
| Exit 2: Graph / token errors | Client secret expired, wrong tenant, or Sites.Read.All not admin-consented. Create a new secret and re-run setup |
| Deleted sites keep appearing / jobs show warnings for missing sites | Enable the Graph deleted-site check |
| Sites added that are already backed up | An Entire Organization job exists; see the warning in the log |
-ExcludeUrlPattern seems ignored | Patterns are wildcards matched against the full URL. Include * at the start, e.g. */sites/test-* |
Task shows 0x1 or other code but no log | The script didn't start: check the path in the task, that -File is used, and execution policy |
| Event log warning in the log | The event log source doesn't exist and the account can't create it. See step 4 |
Maintenance
- Client secret expiry: Graph client secrets expire. Note the expiry date and re-run the
-SetupGraphCredentialstep with the new secret before it lapses, or the run will fail with exit code 2. - Changing the task account: re-create
graph.credwhile logged on as the new account. - Job size: keep an eye on how many objects end up in the target job. If it grows large, consider splitting sites across several jobs and pointing the script at a new one.